/**
*    Copyright (C) 2008  Chase Kernan 
*    chase.kernan@gmail.com
*
*    This program is free software: you can redistribute it and/or modify
*    it under the terms of the GNU General Public License as published by
*    the Free Software Foundation, either version 3 of the License, or
*    (at your option) any later version.
*
*    This program is distributed in the hope that it will be useful,
*    but WITHOUT ANY WARRANTY; without even the implied warranty of
*    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*    GNU General Public License for more details.
*
*    You should have received a copy of the GNU General Public License
*    along with this program.  If not, see <http://www.gnu.org/licenses/>.
* 
**/

package com.chasekernan.hxnova.core.designs;

import com.chasekernan.hxnova.core.races.Race;
import haxe.xml.Check;
import com.chasekernan.hxnova.core.components.Armor;
import com.chasekernan.hxnova.core.components.Bomb;
import com.chasekernan.hxnova.core.components.BeamWeapon;
import com.chasekernan.hxnova.core.components.Component;
import com.chasekernan.hxnova.core.components.Missile;
import com.chasekernan.hxnova.core.components.ComponentSet;
import com.chasekernan.hxnova.core.components.Computer;
import com.chasekernan.hxnova.core.components.Engine;
import com.chasekernan.hxnova.core.components.Hull;
import com.chasekernan.hxnova.core.components.Shield;
import com.chasekernan.hxnova.core.minerals.MineralSet;
import com.chasekernan.hxnova.core.races.TechLevel;
import com.chasekernan.hxnova.core.components.CargoPod;
import com.chasekernan.hxnova.core.races.ResearchCosts;
import com.chasekernan.hxnova.core.components.Scanner;

/**
    A design is built from a collection of these.
**/
typedef DesignPart = {
    var amount : Int;
    var component : Component;
}

/**
    Based off of a hull, this implements a specific ship or starbase design and 
    allows for easy editing by a design editor.
    The slots on the hull are stored in array so they can be accessed by slot 
    number.
**/
class HullDesign {
    
    public static var MAX_COMBAT_SPEED = 2.5;
    public static var MIN_COMBAT_SPEED = 0.25;
    
    public var baseHull : Hull;
    public var slots : Array<DesignPart>;
    public var name : String;
    
    public function clone() : HullDesign {
        var designCopy = new HullDesign(name, baseHull);
        
        for (i in 0...slots.length) {
            designCopy.changeSlot(i, slots[i].component, slots[i].amount); 
        }
        
        return designCopy;
    }
    
    /**
        Creates a new empty design based off of [baseHull].
        Will throw an error if [baseHull] is null.
    **/
    public function new(name : String, baseHull : Hull) {
        this.name = name;
        if(baseHull == null) throw "Base hull cannot be null.";
        this.baseHull = baseHull;
        
        slots = new Array<DesignPart>();
        for(i in baseHull.parts) slots.push({amount : 0, component : null});
    }
    
    /**
        Edits the properties of a given slot.
        Will throw an error if there are too many (or too few) components or if
        the slot type doesn't allow that kind of component.
        Note: Too few is defined as less than one.
        If you want to clear the slot, then use [setSlotToAmount].
    **/
    public function changeSlot(slotNumber : Int, component : Component, 
            amount : Int) {
                
        if(!baseHull.parts[slotNumber].type.hasType(Type.getClass(component))) 
            throw "Slot #"+slotNumber+"'s component type does not match the " + 
                    "given component: "+component.name;
        if(amount < 1) throw "Cannot set a slot amount to less than 1";
        if(amount > baseHull.parts[slotNumber].amount) 
            throw "Cannot add more components than the maximum defined by " + 
                    "the base hull.";
        
        slots[slotNumber] = {amount : amount, component : component};
    }
    
    /**
        Sets a slot to a specified amount. 
        Will throw an error if the component type in that slot is not defined or
        if [amount] is less than 0..
        Note: It will clear the slot if the amount given is 0.
    **/
    public function setSlotToAmount(slotNumber : Int, amount : Int) {
        if(amount == 0) {
            slots[slotNumber] = {amount : 0, component : null}; 
            return;
        }
        
        if(slots[slotNumber].component == null) 
            throw "Cannot set the amount of a slot where the component has " +
                    "not been defined.";
        if(amount < 0) throw "Amount cannot be less than 0.";
        
        slots[slotNumber].amount = amount;
    }
    
    /**
        Returns the total _base_ cost of the design, excluding any modifications
        from a specific race effect.
    **/
    public function getTotalCost() : 
            {resourceCost : Int, mineralCost : MineralSet} {
                
        var cost = {resourceCost : baseHull.resourceCost, 
                    mineralCost : baseHull.mineralCost};
        for(i in slots) {
            if(i.amount == 0) continue;
            cost.resourceCost += i.component.resourceCost * i.amount;
            cost.mineralCost.add(i.component.mineralCost.multiply(i.amount));
        }
        
        return cost;
    }
    
    /**
        Returns if the design is legal (i.e. if the hull has engines, then the 
        design needs engines)
    **/
    public function isLegal() : Bool {
        if (baseHull.isStarbase()) return true;
        for(i in slots) {if(Type.getClass(i.component) == Engine) return true;}
        return false;
    }
    
    public function getTotalMass() : Int {
        var m = baseHull.mass;
        for(i in slots) {
            if(i.component != null) m += i.component.mass * i.amount;
        }
        return m;
    }
    
    /**
        Returns the tech level needed to produce this ship.
    **/
    public function getTechLevelNeeded() : TechLevel {
        var tl : TechLevel = baseHull.requiredTech.clone();
        for(i in ResearchCosts.TECH_TYPES) {
            for(j in slots) {
                if(j.component.requiredTech.getLevel(i) > tl.getLevel(i)) 
                    tl.setLevel(i, j.component.requiredTech.getLevel(i));
            }
        }
        
        return tl;
    }
    
    /**
        Returns the range of the design's normal scanners.
    **/
    public function getNormalScanningRange() : Int {
        var range : Int = 0;
        
        for (slot in slots) {
            if (Type.getClass(slot.component) == Scanner) {
                var scanner = cast(slot.component, Scanner);
                range = calcAdditionalScanners(range, scanner.range, 
                                               slot.amount);
            }
        }
        
        return range;
    }
    
    /**
        Returns the range of the design's penetrating scanners.
    **/
    public function getPenetratingScanningRange() : Int {
        var range : Int = 0;
        
        for (slot in slots) {
            if (Type.getClass(slot.component) == Scanner) {
                var scanner = cast(slot.component, Scanner);
                range = calcAdditionalScanners(range, scanner.penetratingRange, 
                                               slot.amount);
            }
        }
        
        return range;
    }
    
    private static inline function calcAdditionalScanners(base : Int, 
            next : Int, count : Int) : Int {
        //(base ^ 4 + (next ^ 4) * count) ^ (1 / 4)
        return if (next <= 0) base else Math.round(
                Math.pow(Math.pow(base, 4) + Math.pow(next, 4) * count, 0.25));
    }
    
    public function getArmor() : Int {
        var a = baseHull.baseArmor;
        for(i in slots) {
            if (Type.getClass(i.component) == Armor) 
                a += cast(i.component, Armor).strength * i.amount;
            else if (Type.getClass(i.component) == Shield) 
                a += cast(i.component, Shield).strengthArmor * i.amount;
        }
        
        return a;
    }
    
    public function getShields() : Int {
        var s = 0;
        for(i in slots) {
            if (Type.getClass(i.component) == Shield) 
                s += cast(i.component, Shield).strength * i.amount;
            else if (Type.getClass(i.component) == Armor) 
                s += cast(i.component, Armor).strengthShield * i.amount;
        }
        
        return s;
    }
    
    public function hasWeapons() : Bool {
        if(!baseHull.hasWeapons()) return false;
        for(i in slots) {
            if(ComponentSet.getComponentTypeSetByName('Weapon').hasType(
                    Type.getClass(i.component))) return true; 
        }
        
        return false;
    }
    
    public function hasBombingCapabilities() : Bool {
        if (baseHull.isStarbase()) return false;
        
        for(i in slots) if(Type.getClass(i.component) == Bomb) return true;
        
        return false;
    }
    
    public function isStarbase() : Bool {
        return baseHull.isStarbase();
    }
    
    /**
        Returns null if there is no engine or if th design is a starbase.
    **/
    public function getEngineType() : Engine {
        if (baseHull.isStarbase()) return null;
        for(i in slots) {
            if(Type.getClass(i.component) == Engine) return cast i.component;
        }
        return null;
    }
    
    /**
        Returns the number of engines in the design.
    **/
    public function getNumberOfEngines() : Int {
        if (baseHull.isStarbase()) return 0;
        
        var count = 0;
        for(slot in slots) {
            if (Type.getClass(slot.component) == Engine) count++;
        }
        return count;
    }
    
    /**
        Returns the combat speed of the design. If the design is carrying 
        any cargo, cargoMass should be set to reflect the change in battle
        speed from the additional weight.
    **/
    public function getCombatSpeed(?cargoMass = 0) : Float {
        if (baseHull.isStarbase()) return 0;
        
        //TODO: add in thrusters
        var speed = 
                (getEngineType().combatSpeed - 4) / 4 - 
                (getTotalMass() + cargoMass) / 70 / 4 / getNumberOfEngines();
        
        //peg between min and max sped
        speed = Math.max(MIN_COMBAT_SPEED, Math.min(MAX_COMBAT_SPEED, speed));
        return speed;   
    }
    
    //TODO: Implement this function
    public function getRating() : Int {
        return 1;
    }
    
    /**
        Returns true if the design is the same as the one give.
        Note: It is not the same as this == hd.
    **/
    public function isEqualTo(hd : HullDesign) : Bool {
        if (name != hd.name) return false;
        if (baseHull != hd.baseHull) return false;
        //slots are all in the same order so this makes comparisons easy.
        for (i in 0...slots.length) {
            if (slots[i].amount != hd.slots[i].amount) return false;
            if (slots[i].component != hd.slots[i].component) return false;
        }
        
        return true;
    }
    
    /**
        Returns the _cargo_ capacity, not the fuel capacity.
    **/
    public function getCargoCapacity() : Int {
        if (isStarbase()) return 0;
        
        var cap = baseHull.getTotalStorageSpace();
        for(i in slots) {
            if(Type.getClass(i.component) == CargoPod) 
                cap += cast(i.component, CargoPod).mineralCapacity;
        }
        
        return cap;
    }
    
    /**
        Returns true if the design has a scanner.
        NOTE: This is used so that range 0 scanners (ie bat scanner)
        can be detected and used by the scanner functions.
    **/
    public function hasScanner() : Bool {
        for (slot in slots) {
            if (Type.getClass(slot.component) == Scanner) return true;
        }
        
        return false;
    }
    
    /**
        Returns the total fuel capacity of the design (this includes fuel 
        capacity granted by components).
    **/
    public function getFuelCapacity() : Float {
        if (isStarbase()) return 0;
        
        var fuel = baseHull.fuelCapacity;
        for(i in slots) {
            if(Type.getClass(i.component) == CargoPod) 
                fuel += cast(i.component, CargoPod).fuelCapacity;
        }
        
        return fuel;
    }
    
    /**
        Returns true if the race given can build this design (regardless of 
        their current tech level).
    **/
    public function isAvailableTo(race : Race) : Bool {
        if (!baseHull.isAvailableTo(race)) return false;
        for(i in slots) {if(!i.component.isAvailableTo(race)) return false;}
        return true;
    }
    
    /**
        Returns the computers that are used in this design.
        Note: If a slot has more than one computer, then the design is added
        multiple times to the array.
    **/
    public function getComputers() : Array<Computer> {
        var computers = new Array<Computer>();
        
        for (slot in slots) {
            if (Type.getClass(slot.component) == Computer) {
                for (i in 0...slot.amount) computers.push(cast slot.component);
            }
        }
        
        return computers;
    }
    
    public function getWeapons() : Array<Component> {
        var weapons = new Array<Component>();
        
        for (slot in slots) {
            if (switch (Type.getClass(slot.component)) {
                    
                case untyped BeamWeapon: true;
                case untyped Missile: true;
                default: false;
                    
            }) weapons.push(slot.component);
        }
        
        return weapons;
    }
    
    public static var XML_TITLE = "ship_design";
    
    public static function getXmlRules() : Rule {
        return RNode(XML_TITLE, [Att('name'), Att('base_hull')], 
                RMulti(RNode('slot', [Att('amount', FInt), Att('slot_num'), 
                                      Att('component')]), false));
    }
    
    /**
        Writes a ship design to an xml file.
        Sample xml:
            TODO: Run [writeToXml]
    **/
    public static function writeToXml(d : HullDesign) : Xml {
        var xml = Xml.parse('<' + XML_TITLE + '></' + XML_TITLE + '>')
                .firstChild();
        xml.set('name', d.name);
        xml.set('base_hull', d.baseHull.name);
        
        if (d.slots != null) {
            for(i in 0...d.slots.length) {
                if(d.slots[i].component == null) continue;
            
                var child = Xml.createElement('slot');
                child.set('slot_num', Std.string(i));
                child.set('amount', Std.string(d.slots[i].amount));
                child.set('component', Std.string(d.slots[i].component.name));
            
                xml.addChild(child);
            }
        }
        
        Check.checkNode(xml, getXmlRules());
        
        return xml;
    }
    
    /**
        Reads a hull design from an xml file.
        Will throw an error if there are any mistakes.
    **/
    public static function readFromXml(xml : Xml) : HullDesign {
        try {
            Check.checkNode(xml, getXmlRules());
            
            var name = xml.get('name');
            var baseHull = cast ComponentSet.getComponentByName(
                    xml.get('base_hull'));
            
            var hd : HullDesign = new HullDesign(name, baseHull);
            
            var foundSlot = false;
            for(i in xml.elementsNamed('slot')) {
                foundSlot = true;
                hd.changeSlot(Std.parseInt(i.get('slot_num')), 
                        ComponentSet.getComponentByName(i.get('component')), 
                        Std.parseInt(i.get('amount')));
            }
            
            if (!foundSlot) hd.slots = null;
            
            return hd;
                
        } catch(e : Dynamic) {
            throw "Error while parsing ship design: "+e;
        }
    }
}